#!/bin/sh

#
# Test suCGI.
#
# Copyright 2023 Odin Kroeger.
#
# This file is part of suCGI.
#
# suCGI is free software: you can redistribute it and/or modify it
# under the terms of the GNU Affero General Public License as published
# by the Free Software Foundation, either version 3 of the License,
# or (at your option) any later version.
#
# suCGI is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
# or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General
# Public License for more details.
#
# You should have received a copy of the GNU Affero General Public
# License along with suCGI. If not, see <https://www.gnu.org/licenses/>.
#

# shellcheck disable=2015,2016,2034


#
# Initialiation
#

set -Cefu
scriptdir="$(cd -P "$(dirname -- "$0")" && pwd)"
: "${scriptdir:?}"
readonly scriptdir
: "${srcdir:="$(cd -P "$scriptdir/.." && pwd)"}"
: "${srcdir:?}"
readonly srcdir

for dir in . ./bin ./tests/bin "$srcdir/tests/bin" "$scriptdir/bin"
do
	if [ -e "$dir/sucgi" ]
	then bindir="$dir"
	fi
done
readonly bindir

for dir in . ./utils ./tests/utils "$srcdir/tests/utils" "$scriptdir/utils"
do
	if [ -e "$dir/uids" ]
	then utildir="$dir"
	fi
done
readonly utildir

# shellcheck disable=2154
export PATH="$utildir:$bindir:$PATH"

# shellcheck disable=1091
. "$srcdir/libutil.sh"	|| exit
init			|| exit
# shellcheck disable=2119
tmpdir			|| exit

# Always exit with a status indicating failure unless $fin is set.
atexit="${atexit-:}; [ \"\${fin-}\" ] || exit 69"


#
# Environment
#

# Only use libmock.so for sucgi, sometimes.
# System utilities tend to by-pass mock-ups.
unset LD_PRELOAD DYLD_INSERT_LIBRARIES


#
# Constants
#

# System name.
uname="$(uname)"
readonly uname
[ "$uname" ] || err "Could not determine system name"


#
# Globals
#

# Overall result.
result=0

# Number of skipped tests.
nskipped=0


#
# Functions
#

# Starting with, but excluding, $1, run $3 for every path segment of $2,
# where $1 is a directory that contains the file $2, and $3 is a command.
# If $4 is given, run $3 for every path segment up to, but excluding $2,
# and run $4 for $2.
dirwalk() (
	dirname="${1:?}"
	fname="${2:?}"
	__dirwalk_dircmd="${3:?}"
	__dirwalk_fcmd="${4:-"$3"}"

	IFS=/
	# shellcheck disable=2086
	set -- ${fname#"${dirname%/}/"}
	unset IFS
	cd "$dirname" || exit

	while [ "$#" -gt 0 ]
	do
		fname="$1"
		shift
		[ "$fname" ] || continue

		case $# in
		(0)
			eval "$__dirwalk_fcmd"
			return
			;;
		(*)
			eval "$__dirwalk_dircmd"
			dirname="${dirname%/}/$fname"
			cd "$fname" || return
			;;
		esac
	done
)

# Create a filename, not a file, in $1 that is $2 characters long.
mkfname() (
	__mkfname_base="${1:?}"
	__mkfname_len="${2:?}"
	__mkfname_max=99999
	__mkfname_namemax="$(
		getconf NAME_MAX "$__mkfname_base" 2>/dev/null
	)" || __mkfname_namemax=14

	__mkfname_dirs=$((__mkfname_len / __mkfname_namemax + 1))
	__mkfname_dirlen=$((__mkfname_len / __mkfname_dirs))

	__mkfname_dir="$__mkfname_base"
	__mkfname_targetlen=$((__mkfname_len - __mkfname_dirlen - 1))
	while [ "${#__mkfname_dir}" -le "$__mkfname_targetlen" ]
	do
		__mkfname_i=0
		while [ "$__mkfname_i" -lt "$__mkfname_max" ]
		do
			__mkfname_seg="$(
				pad "$__mkfname_dirlen" "$__mkfname_i"
			)"
			if ! [ -e "$__mkfname_dir/$__mkfname_seg" ]
			then
				__mkfname_dir="$__mkfname_dir/$__mkfname_seg"
				continue 2
			fi
			__mkfname_i=$((__mkfname_i + 1))
		done
	done

	__mkfname_i=0 __mkfname_fname=
	while [ "$__mkfname_i" -lt "$__mkfname_max" ]
	do
		__mkfname_seg="$(
			pad "$((__mkfname_len - ${#__mkfname_dir} - 1))" \
			    "$__mkfname_i"
		)"
		if ! [ -e "$__mkfname_dir/$__mkfname_seg" ]
		then
			__mkfname_fname="$__mkfname_dir/$__mkfname_seg"
			break
		fi
		__mkfname_i=$((__mkfname_i + 1))
	done

	# This should be guaranteed, but who knows?
	[ "${#__mkfname_fname}" -eq "$__mkfname_len" ] ||
		err 'Failed to generate filename'

	printf '%s\n' "$__mkfname_fname"
)

# Take filenames and print non-canonical mutations of those filenames.
mutatefnames() {
	printf '%s\n' "$@"				|
	sed -n 'p; s/^\([^/]\)/.\/\1/p'			|
	sed -n 'p; s/\//\/.\//p'			|
	sed -n 'p; s/\/\([^/]*\)\//\/\1\/..\/\1\//p'
	# / -> // cannot be tested because musl's realpath may leave // as is.
}

# Pad $2 with $3 on side $4 up to length $1.
pad() (
	__pad_n="${1:?}"
	__pad_str="${2-}"
	__pad_fill="${3:-x}"
	__pad_side="${4:-l}"
	__pad_strlen="${#__pad_str}"
	__pad_fillen="${#__pad_fill}"
	__pad_limit="$((__pad_n - __pad_fillen))"
	
	__pad_padding=
	while [ "$__pad_strlen" -le "$__pad_limit" ]
	do
		__pad_padding="$__pad_padding$__pad_fill"
		__pad_strlen=$((__pad_strlen + __pad_fillen))
	done

	case $__pad_side in
	(l) printf '%s%s\n' "$__pad_padding" "$__pad_str" ;;
	(r) printf '%s%s\n' "$__pad_str" "$__pad_padding" ;;
	esac
)

# Delete every segment of $1 up to $2, regardless of PATH_MAX, if possible.
# shellcheck disable=2317
rmtree() (
	__rmtree_path="${1:?}"
	__rmtree_stop="${2:?}"
	while true
	do
		case $__rmtree_path in
		($__rmtree_stop/*) : ;;
		(*)                break ;;
		esac

		dirwalk "$__rmtree_stop" "$__rmtree_path" : 'rm -rf "$fname"'
		__rmtree_path="$(dirname "$__rmtree_path")"
	done
)

# Find an unallocated user ID in the range $1 to $2.
unallocuid() (
	__unallocuid_start="${1:?}"
	__unallocuid_end="${2:?}"
	__unallocuid_uids="$(uids | awk '{print $1}')"
	
	__unallocuid_uid="$__unallocuid_start"
	while [ "$__unallocuid_uid" -le "$__unallocuid_end" ]
	do
		# shellcheck disable=2086
		if ! inlist -eq "$__unallocuid_uid" $__unallocuid_uids
		then
			printf '%d\n' "$__unallocuid_uid"
			return 0
		fi

		__unallocuid_uid=$((__unallocuid_uid + 1))
	done
	return 1
)


#
# Options
#

OPTIND=1 OPTARG='' opt=''
while getopts dh opt
do
	# shellcheck disable=2154
	case $opt in
	(h) exec cat <<EOF ;;
$progname - test the sucgi binary

Usage:  $progname [-d]
        $progname -h

Options:
    -d  Enable debugging mode.
    -h  Show this help screen.
EOF
	(d) set -x ;;
	(*) exit 2
	esac
done
shift $((OPTIND - 1))
unset opt


#
# Build configuration
#

eval "$(sucgi -c | grep -vE '^PATH=')"


#
# Common setup for non-privileged tests
#

# Current user.
logname="$(id -un)"
uid="$(id -u)"


#
# Environment sanitisation
#

# Too many variables.
i=0 envsan_vars=
while [ "$i" -le "$MAX_NVARS" ]
do
	i=$((i + 1))
	envsan_vars="$envsan_vars var$i="
done
unset i

# shellcheck disable=2086
check -s1 -e'Too many environment variables' \
      $envsan_vars sucgi		|| result=70

# Variable name has maximum length.
envsan_varname="$(pad $((MAX_VARNAME_LEN - 1)) v)"
check -s1 -e"Discarding \$$envsan_varname" \
      "$envsan_varname=" sucgi	|| result=70

# Variable name is too long.
envsan_varname="$(pad "$MAX_VARNAME_LEN" v)"
envsan_var="$envsan_varname=foo"
check -s1 -e'Discarding variable with long name' \
      "$envsan_var" sucgi		|| result=70

# Variable has maximum length.
envsan_var="$(pad $((MAX_VAR_LEN - 1)) var= x r)"
check -s1 -e"Discarding \$${envsan_var%=*}" \
      "$envsan_var" sucgi		|| result=70

# Variable is too long.
envsan_var="$(pad "$MAX_VAR_LEN" var= x r)"
check -s1 -e'Discarding long variable'	\
      "$envsan_var" sucgi		|| result=70

# Variable name is illegal.
for envsan_varname in 'foo ' '0foo' '$(echo foo)' '`echo foo`'
do
	check -s1 -e"\$$envsan_varname: Bad name" \
	      badenv PATH="$PATH" "$envsan_varname=" "$bindir/sucgi" || result=70
done

# Variable is not of the form <key>=<value>.
for envsan_var in foo bar baz
do
	check -s1 -e"\$$envsan_var: No value" \
	      badenv -n2 PATH="$PATH" "$envsan_var" "$bindir/sucgi" || result=70
done

# Not a CGI variable.
check -s1 -e'Discarding $foo' foo=foo sucgi || result=70

# Cleanup.
unset envsan_var envsan_varname envsan_vars


#
# Missing argv[0]
#

case $uname in
(*BSD) : ;; # BSD's execvp does not accept NULL as *argv.
(*)    check -s1 -e'Empty argument vector' badexec sucgi || result=70
esac

check -s1 -e'Empty argument vector' badexec sucgi '' || result=70


#
# Option handling
#

# Help.
check -o'Print this help screen.' sucgi -h || result=70

# Version.
check -o'suCGI ' sucgi -v || result=70

# Build configuration.
(
	eval "$(sucgi -c | grep -vE ^PATH=)" || exit 70
	for opthdl_var in \
		SCRIPT_HANDLERS \
		MAX_ERRMSG_LEN \
		MAX_FNAME_LEN \
		MAX_GID_VAL \
		MAX_GRP_VAL \
		MAX_NGROUPS \
		MAX_NGRPS_VAL \
		MAX_NVARS \
		MAX_STR_LEN \
		MAX_STR_LEN \
		MAX_SUFFIX_LEN \
		MAX_UID_VAL \
		MAX_VAR_LEN \
		MAX_VARNAME_LEN \
		NATTR \
		NDEBUG \
		PATH \
		SAFE_ENV_VARS \
		START_GID \
		START_UID \
		STOP_GID \
		STOP_UID \
		SYSLOG_FACILITY \
		SYSLOG_MASK \
		SYSLOG_OPTS \
		TESTING \
		UMASK \
		USER_DIR
	do
		eval val="\${$opthdl_var-}"
		if ! [ "${val-}" ]
		then err -s70 'sucgi -c: %s: Undefined' "$opthdl_var"
		fi
	done
) || result=70

# Usage message.
for opthdl_args in -V -C -H -X -XX -x --x - -- '-h -c' '-hc' '-h -v' '-hc'
do
	# shellcheck disable=2086
	check -s2 -e'usage: sucgi' sucgi $opthdl_args || result=70
done

check -s2 -e'usage: sucgi' sucgi '' || result=70

# Cleanup.
unset opthdl_opt opthdl_args opthdl_var


#
# Script filename sanitisation
#

# Create a path that is as long as suCGI permits.
scrsan_varname='PATH_TRANSLATED='
scrsan_maxlen="$((MAX_VAR_LEN - ${#scrsan_varname}))"
[ "$scrsan_maxlen" -gt "$MAX_FNAME_LEN" ] && scrsan_maxlen="$MAX_FNAME_LEN"
scrsan_longpath="$(mkfname "$TMPDIR" "$((scrsan_maxlen - 1))")"
mkdir -p "$(dirname -- "$scrsan_longpath")"
touch "$scrsan_longpath"
unset scrsan_varname scrsan_maxlen

# Create a path that is longer than suCGI permits.
scrsan_hugepath="$(mkfname "$TMPDIR" "$MAX_FNAME_LEN")"
readonly scrsan_hugepath
catch=
dirwalk "$TMPDIR" "$scrsan_hugepath" 'mkdir "$fname"' 'touch "$fname"'
atexit="rmtree \"$scrsan_hugepath\" \"$TMPDIR\"; ${atexit-:}"
catch=y
[ "$caught" ] && kill -s"$caught" "$$"

# Create a link to that path.
catch=
scrsan_hugelink="$TMPDIR/$(dirwalk "$TMPDIR" "$scrsan_hugepath" \
	'ln -s "$fname" s.d && printf s.d/' \
	'ln -s "$fname" s.f && printf s.f\\n')"
readonly scrsan_hugelink
atexit="rmtree \"$scrsan_hugelink\" \"$TMPDIR\"; ${atexit-:}"
catch=y
[ "$caught" ] && kill -s"$caught" "$$"

# Create a file of the wrong type.
scrsan_wrongftype="$TMPDIR/scrsan-wrongftype"
mkdir "$scrsan_wrongftype"

# Create a valid script.
scrsan_script="$TMPDIR/scrsan-valid.php"
touch "$scrsan_script"

# PATH_INFO is undefined.
check -s1 -e'$PATH_INFO: Not set' \
	sucgi						|| result=70

# $PATH_INFO is empty.
check -s1 -e'$PATH_INFO: Empty' \
	PATH_INFO= sucgi				|| result=70

# PATH_TRANSLATED is undefined.
check -s1 -e'$PATH_TRANSLATED: Not set' \
	PATH_INFO=foo \
	sucgi						|| result=70

# $PATH_TRANSLATED is empty.
check -s1 -e'$PATH_TRANSLATED: Empty' \
	PATH_INFO=foo \
	PATH_TRANSLATED= \
	sucgi						|| result=70

# $PATH_TRANSLATED is too long.
check -s1 -e'long' \
	PATH_INFO=foo \
	PATH_TRANSLATED="$scrsan_hugepath" \
	sucgi						|| result=70

# Path to script is too long after having been resolved.
check -s1 -e'long' \
	PATH_INFO=foo \
	PATH_TRANSLATED="$scrsan_hugelink" \
	sucgi						|| result=70

# Script is of the wrong type.
check -s1 -e"script $scrsan_wrongftype: Not a regular file" \
	PATH_INFO=foo \
	PATH_TRANSLATED="$scrsan_wrongftype" \
	sucgi						|| result=70

# Script does not exist.
check -s1 -e"realpath $TMPDIR/<nosuchfile>: No such file or directory" \
	PATH_INFO=foo \
	PATH_TRANSLATED="$TMPDIR/<nosuchfile>" \
	sucgi						|| result=70

# Error is system-dependent.
case $uid in
(0)
	scrsan_err="Owned by privileged user"
	;;
(*)
	isreguser=x logname="$(id -un)"
	if	[ "$uid" -lt "$START_UID" ] ||
		[ "$uid" -gt "$STOP_UID" ]
	then
		isreguser=
	else
		for gid in $(id -G "$logname")
		do
			if	[ "$gid" -lt "$START_GID" ] ||
				[ "$gid" -gt "$STOP_GID" ]
			then
				isreguser=
				break
			fi
		done
	fi
	unset gid

	if [ "$isreguser" ]
	then scrsan_err='seteuid: Operation not permitted'
	else scrsan_err="user $logname: Member of system group"
	fi
	;;
esac

# PATH_TRANSLATED is valid, so another error should be raised.
mutatefnames "$scrsan_script" |
while read -r scrsan_fname
do
	check -s1 -e"$scrsan_err" \
	      PATH_INFO=foo \
	      PATH_TRANSLATED="$scrsan_fname" \
	      sucgi || exit 1
done || result=70

# $PATH_TRANSLATED is valid despite its long name.
if [ "${LIBC-}" ]
then
	check -s1 -e"$scrsan_err" \
		PATH_INFO=foo \
		PATH_TRANSLATED="$scrsan_longpath" \
		sucgi || result=70
else
	# musl truncates long syslog messages.
	warn 'LibC may be musl; skipping check for long filename reporting'
	nskipped=$((nskipped + 1))
fi

# Cleanup.
unset scrsan_err scrsan_longpath scrsan_script scrsan_fname scrsan_wrongftype


#
# Stop unless run by the superuser
#

if [ "$uid" -ne 0 ]
then
        fin=y

	case $result in
	(0) err -s75 'Skipping superuser tests' ;;
	(*) err -s"$result" 'Skipping superuser tests' ;;
	esac
fi


#
# User validation
#

# Create a script.
usrval_script="$TMPDIR/usrval-script.sh"
touch "$usrval_script"

# Find an unallocated user ID.
usrval_unallocuid="$(unallocuid "$START_UID" "$STOP_UID")"

# Store a list of all user IDs.
usrval_uids="$TMPDIR/usrval-uids.list"
uids >"$usrval_uids"

# Find a user with an ID < $START_UID
usrval_lowuid="$(
	awk -vmax="$START_UID" \
	    '0 < $1 && $1 < max {print $2; exit}' "$usrval_uids"
)"

# Find a user with an ID > $STOP_UID
usrval_highuid="$(
	awk -vmin="$STOP_UID" \
	    '$1 > min {print $2; exit}' "$usrval_uids"
)"

# Owned by non-existing user.
chown "$usrval_unallocuid" "$usrval_script"
check -s1 -e"script $usrval_script: No owner" \
      PATH_INFO=foo \
      PATH_TRANSLATED="$usrval_script" \
      sucgi || result=70

# Owned by a privileged user
mutatefnames "$usrval_script" |
while read -r usrval_fname
do
	for usrval_uid in 0 "$usrval_lowuid" "$usrval_highuid"
	do
		[ "$usrval_uid" ] || continue

		chown "$usrval_uid" "$usrval_script"

		check -s1 \
		      -e"script $usrval_fname: Owned by privileged user" \
		      PATH_INFO=foo \
		      PATH_TRANSLATED="$usrval_fname" \
		      sucgi
	done
done

# Cleanup.
unset usrval_script usrval_fname usrval_uid usrval_lowuid usrval_highuid \
      usrval_uids usrval_unallocuid


#
# Common setup for privileged tests
#

# Search for a regular user.
if ! reguser="$(reguser "$START_UID" "$STOP_UID" "$START_GID" "$STOP_GID")"
then
	fin=y

	case $result in
	(0) err -s75 'No regular user found' ;;
	(*) err -s"$result" 'No regular user found' ;;
esac
fi

runas "$reguser" sucgi -c >/dev/null 2>&1 || {
	fin=y
	err -s75 '%s: Cannot execute sucgi' "$reguser"
}

reguid="$(id -u "$reguser")"
reggid="$(id -g "$reguser")"

# The user directory is hard-coded for testing.
userdir="/tmp/sucgi-check/$reguser"

# Create another temporary directory.
tmpdir="${userdir%/"$reguser"*}"
case $tmpdir in
	(/tmp/*)	: ;;
	(*)		err '%s is outside of /tmp' "$tmpdir"
esac
readonly tmpdir

catch=
mkdir -m 0755 "$tmpdir"
atexit="rm -rf \"\$tmpdir\"; ${atexit}"
catch=y
[ "$caught" ] && kill -s"$caught" "$$"

TMPDIR="$tmpdir"
export TMPDIR

# Create the user directory.
mkdir -p "$userdir"
userdir="$(cd -P "$userdir" && pwd)" && [ "$userdir" ] || exit
readonly userdir

# Create a script that prints process IDs.
readonly procids_sfx="$userdir/ids.sh"
cat <<'EOF' >"$procids_sfx"
#!/bin/sh
printf 'uid=%s egid=%s ruid=%s rgid=%s\n' \
       "$(id -u)" "$(id -g)" "$(id -ur)" "$(id -gr)"
EOF
chmod -x "$procids_sfx"

# The same script w/o a suffix.
readonly procids_nosfx="$userdir/ids"
cp "$procids_sfx" "$procids_nosfx"
chmod +x "$procids_nosfx"

# Create a script that prints the environment.
readonly penv_sfx="$userdir/penv.sh"
cat <<'EOF' >"$penv_sfx"
#!/bin/sh
env
EOF
chmod -x "$penv_sfx"

# The same script w/o a suffix.
readonly penv_nosfx="$userdir/penv"
cp "$penv_sfx" "$penv_nosfx"
chmod +x "$penv_nosfx"

# Adapt ownership.
chown -R "$reguser" "$userdir"


#
# Non-superuser
#

check -s1 -e'seteuid: Operation not permitted' \
	PATH_INFO=foo \
	PATH_TRANSLATED="$penv_sfx" \
	runas "$reguser" sucgi


#
# Privilege dropping
#

priv_output="uid=$reguid egid=$reggid ruid=$reguid rgid=$reggid"
for priv_script in "$procids_sfx" "$procids_nosfx"
do
	# /tmp might be mounted noexec.
	if [ -x "$priv_script" ] && ! $priv_script >/dev/null 2>&1
	then
		warn '/tmp may be noexec; skipping check for privilege drop'
		nskipped=$((nskipped + 1))
		continue
	fi

	#mutatefnames "$priv_script" |
	echo "$priv_script" |
	while read -r priv_fname
	do
		check -s0 -o"$priv_output" \
			PATH_INFO="/~$reguser${priv_fname#"$userdir"}" \
			PATH_TRANSLATED="$priv_fname" \
			sucgi || exit 2
	done || result=70
done
unset priv_script priv_fname priv_output


#
# User directory validation
#

# Create a link from outside to inside the user directory.
dirval_outtoin="$tmpdir/dirval-out-to-in.sh"
ln -s "$procids_sfx" "$dirval_outtoin"

# Create a script outside the user directory.
dirval_outside="$tmpdir/dirval-outside.sh"
touch "$dirval_outside"
chown "$reguser" "$dirval_outside"

# Pretend the script is inside using dots.
dirval_dots="$userdir/../dirval-outside.sh"
touch "$dirval_dots"
chown "$reguser" "$dirval_dots"

# Create a link from inside to outside the user directory.
dirval_intoout="$userdir/in-to-out.sh"
ln -s "$dirval_outside" "$dirval_intoout"

# Forbidden location.
mutatefnames "$dirval_outside" "$dirval_dots" "$dirval_intoout" |
while read -r dirval_fname
do
	error="script $dirval_fname: Not in $reguser's user directory"
	check -s1 -e"$error" \
		PATH_INFO=foo \
		PATH_TRANSLATED="$dirval_fname" \
		sucgi || exit 2
done

# Permitted location.
dirval_output="uid=$reguid egid=$reggid ruid=$reguid rgid=$reggid"
for dirval_fname in "$dirval_outtoin" # "$procids_sfx" "$dirval_outtoin"
do
	check -s0 -o"$dirval_output" \
		PATH_INFO="/~$reguser${procids_sfx#"$userdir"}" \
		PATH_TRANSLATED="$dirval_fname" \
		sucgi || exit 2
done || result=70

# Cleanup.
unset dirval_outtoin dirval_outside dirval_intoout dirval_output dirval_fname


#
# Set-process-IDs on execute bits
#

# Set-user-ID on execute bit set.
chmod u=rws,g=r,o=r "$procids_sfx"

mutatefnames "$procids_sfx" |
while read -r procids_fname
do
	error="script $procids_fname: Set-user-ID on execute bit set"
	check -s1 -e"$error" \
		PATH_INFO=foo \
		PATH_TRANSLATED="$procids_fname" \
		sucgi || exit 2
done || result=70

# Set-group-ID on execute bit set.
chmod u=rx,g=rs,o=r "$procids_sfx"

mutatefnames "$procids_sfx" |
while read -r procids_fname
do
	error="script $procids_fname: Set-group-ID on execute bit set"
	check -s1 -e"$error" \
		PATH_INFO=foo \
		PATH_TRANSLATED="$procids_fname" \
		sucgi || exit 2
done || result=70

# Cleanup.
chmod u=rwx,go= "$procids_sfx"
unset procids_fname


#
# Hidden scripts
#

# Create a hidden script.
hidden_file="$userdir/.hidden.sh"
touch "$hidden_file"

# Create a script in a hidden directory.
hidden_dir="$userdir/.hidden"
mkdir "$hidden_dir"
hidden_indir="$hidden_dir/script.sh"
touch "$hidden_indir"

# Adapt ownership.
chown -R "$reguser" "$userdir"

# Test.
mutatefnames "$hidden_file" "$hidden_indir" |
while read -r hidden_fname
do
	check -s1 -e"script $hidden_fname: Hidden" \
		PATH_INFO=foo \
		PATH_TRANSLATED="$hidden_fname" sucgi || exit 2
done || result=70

# Cleanup.
unset hidden_file hidden_dir hidden_indir hidden_fname


#
# Wrong PATH_INFO
#

scripturl="/~$reguser${procids_sfx#"$userdir"}"
check -s1 -e"\$PATH_INFO: Does not start with $scripturl" \
	PATH_INFO=/ \
	PATH_TRANSLATED="$procids_sfx" \
	sucgi || result=70

unset scripturl


#
# Script handler
#

# Create a non-executable script w/o a suffix.
hdl_nosuffix="$userdir/script"
touch "$hdl_nosuffix"

# Create a script with an unknown suffix.
hdl_unknownsuffix="$userdir/script.nil"
touch "$hdl_unknownsuffix"

# Create a script for which an empty handler is registered.
hdl_emptyhandler="$userdir/script.empty"
touch "$hdl_emptyhandler"

# Create a script with a too long filename suffix.
hdl_suffix=$(pad "$MAX_SUFFIX_LEN" . x r)
hdl_hugesuffix="$userdir/script$hdl_suffix"
touch "$hdl_hugesuffix"

# Adapt ownership.
chown -R "$reguser" "$userdir"

# Bad handler.
check -s1 -e"script $hdl_emptyhandler: Bad handler" \
	PATH_INFO="/~$reguser${hdl_emptyhandler#"$userdir"}" \
	PATH_TRANSLATED="$hdl_emptyhandler" \
	sucgi || result=70

# Suffix is too long.
check -s1 -e"script $hdl_hugesuffix: Filename suffix too long" \
	PATH_INFO="/~$reguser${hdl_hugesuffix#"$userdir"}" \
	PATH_TRANSLATED="$hdl_hugesuffix" \
	sucgi || result=70

# Unknown suffix.
check -s1 -e"execl $hdl_unknownsuffix: Permission denied" \
	PATH_INFO="/~$reguser${hdl_unknownsuffix#"$userdir"}" \
	PATH_TRANSLATED="$hdl_unknownsuffix" \
	sucgi || result=70

# Not executable.
check -s1 -e"execl $hdl_nosuffix: Permission denied" \
	PATH_INFO="/~$reguser${hdl_nosuffix#"$userdir"}" \
	PATH_TRANSLATED="$hdl_nosuffix" \
	sucgi || result=70

# Cleanup.
unset hdl_nosuffix hdl_unknownsuffix hdl_emptyhandler \
      hdl_suffix hdl_hugesuffix hdl_fname


#
# Environment cleanup.
#

# Get home directory.
eval "envcln_homedir=~$reguser"

for envcln_script in "$penv_sfx" "$penv_nosfx"
do
	# /tmp might be mounted noexec.
	if [ -x "$envcln_script" ] && ! $envcln_script >/dev/null 2>&1
	then
		nskipped=$((nskipped + 1))
		continue
	fi

	envcln_logfile="$envcln_script.log"

	\
		PATH_INFO="/~$reguser${envcln_script#"$userdir"}"	\
		PATH_TRANSLATED="$envcln_script"			\
		CLICOLOR='x'						\
		DOCUMENT_ROOT='/home/jdoe/public_html'			\
		EDITOR='vim'						\
		HTTP_HOST='www.foo.example'				\
		HTTP_REFERER='https://www.bar.example'			\
		HTTP_USER_AGENT='FakeZilla/1'				\
		HTTPS='on'						\
		IFS=':'							\
		LANG='en_GB.UTF-8'					\
		LOGNAME='jdoe'						\
		PAGER='less'						\
		PWD='/home/jdoe'					\
		QUERY_STRING='foo=bar'					\
		REMOTE_ADDR='100::1:2:3'				\
		REMOTE_HOST='100::1:2:3'				\
		REMOTE_PORT=50000					\
		REQUEST_METHOD='GET'					\
		REQUEST_URI='/index.php'				\
		SERVER_ADMIN='admin@foo.example'			\
		SERVER_NAME='www.foo.example'				\
		SERVER_PORT=443						\
		SERVER_SOFTWARE='Apache v2.4' 				\
		USER='jdoe'						\
		VISUAL='vim'						\
		sucgi >"$envcln_logfile" 2>/dev/null	||
		err -s70 'sucgi: Exited with status %d' $?

	# shellcheck disable=2154
	for envcln_var in						\
		"PATH_INFO=/~$reguser${envcln_script#"$userdir"}"	\
		"PATH_TRANSLATED=$envcln_script"			\
		"DOCUMENT_ROOT=$userdir"				\
		"HOME=$envcln_homedir"					\
		'HTTPS=on'						\
		'HTTP_HOST=www.foo.example'				\
		'HTTP_REFERER=https://www.bar.example'			\
		'HTTP_USER_AGENT=FakeZilla/1'				\
		'PATH=/usr/bin:/bin'					\
		'QUERY_STRING=foo=bar'					\
		'REMOTE_ADDR=100::1:2:3'				\
		'REMOTE_HOST=100::1:2:3'				\
		'REMOTE_PORT=50000'					\
		'REQUEST_METHOD=GET'					\
		'REQUEST_URI=/index.php'				\
		"SCRIPT_FILENAME=$envcln_script"			\
		'SERVER_ADMIN=admin@foo.example'			\
		'SERVER_NAME=www.foo.example'				\
		'SERVER_PORT=443'					\
		'SERVER_SOFTWARE=Apache v2.4'				\
		"USER_NAME=$reguser"
	do
		if ! grep -Eq "^$envcln_var$" "$envcln_logfile"
		then
			warn 'PATH_TRANSLATED=%s [...] sucgi: $%s: Not set' \
				"$envcln_script" "$envcln_var"
								result=70
		fi
	done

	for envcln_var in				\
		CLICOLOR=				\
		EDITOR=					\
		IFS=					\
		LANG=					\
		LOGNAME=				\
		PAGER=					\
		USER=jdoe				\
		VISUAL=
	do
		if grep -Eq "^$envcln_var" "$envcln_logfile"
		then
			warn 'PATH_TRANSLATED=%s [...] sucgi: $%s: Set' \
				"$envcln_script" "$envcln_var"
			result=70
		fi
	done
	done

# Cleanup.
unset envcln_homedir envcln_script envcln_logfile envcln_var


#
# Success
#

fin=y

case $result in
(0) [ "$nskipped" -gt 0 ] && err -s75 'Skipped %d test(s)' "$nskipped" ;;
(*) [ "$nskipped" -gt 0 ] && err -s"$result" 'Skipped %d test(s)' "$nskipped" ;;
esac

exit "$result"
